package bg.btanev.eightyfour.dao.impl;

import java.io.Serializable;
import java.sql.SQLException;
import java.util.List;

import org.apache.commons.lang.Validate;
import org.hibernate.HibernateException;
import org.hibernate.LockOptions;
import org.hibernate.Session;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import bg.btanev.eightyfour.dao.GenericDAO;
import bg.btanev.eightyfour.dao.PagingConfig;
import bg.btanev.eightyfour.dao.PagingConfig.OrderConfig;
import bg.btanev.eightyfour.dao.exception.AttachEntityException;
import bg.btanev.eightyfour.dao.exception.DaoInitializationException;
import bg.btanev.eightyfour.dao.exception.DependentObjectExpection;
import bg.btanev.eightyfour.domain.entitybase.DomainEntity;

/**
 * The generic Hibernate DAO implementation with Spring integration.
 *
 * @author B. Tanev
 * @param <ID>
 *            the type of the unique identifier
 * @param <E>
 *            the {@link DomainEntity} type
 */
public class GenericDAOHibernateImpl<ID extends Serializable, E extends DomainEntity<ID>> extends HibernateDaoSupport
		implements GenericDAO<ID, E> {

	/**
	 * Logger for this class
	 */
	protected final Logger logger = LoggerFactory.getLogger(getClass());

	private static final String ID_PROPERTY_NAME = "id";

	private final String entityName;
	private final Class<E> entityClass;

	private boolean flushAfterChange = true;

	@SuppressWarnings("unchecked")
	public GenericDAOHibernateImpl(String entityName) throws DaoInitializationException {
		super();

		Validate.notEmpty(entityName);

		this.entityName = entityName;
		try {
			this.entityClass = (Class<E>) Class.forName(this.entityName);
		} catch (ClassNotFoundException e) {
			String errorMsg = "DAO initialization error: Cannot load entity class: " + entityName; //$NON-NLS-1$
			logger.error(errorMsg, e);
			throw new DaoInitializationException(errorMsg, e);
		}
	}

	/**
	 * Initial method.
	 */
	@Override
	protected void initDao() throws Exception {
		/*
		 * Executes spring's HibernateDaoSupport initDao() method.
		 */
		super.initDao();

		logger.info("Initializing DAO instance for domain entity '{}'", entityName); //$NON-NLS-1$
	}

	@SuppressWarnings("unchecked")
	@Override
	public E findById(ID id) {
		Validate.notNull(id);

		E result = (E) getHibernateTemplate().get(entityName, id);
		logger.debug("Result for an entity object search with id '{}' is: {}", id, result); //$NON-NLS-1$

		return result;
	}

	@Override
	public List<E> findAll() {
		logger.debug("Finding all entities of type '{}'.", entityName); //$NON-NLS-1$
		List<E> result = (List<E>) getHibernateTemplate().loadAll(entityClass);

		return result;
	}

	@Override
	public void flushAndClearSession() {
		logger.debug("Flushing and clearing Hibernate session..."); //$NON-NLS-1$

		flush();
		clearSession();
	}

	@Override
	public E refresh(E entity) {
		Validate.notNull(entity);

		logger.debug("Refreshing entity of type {}...", entityName); //$NON-NLS-1$
		getHibernateTemplate().refresh(entity);

		return entity;
	}

	@Override
	public void saveUpdateEntity(E entity) {
		Validate.notNull(entity);

		try {
			getHibernateTemplate().saveOrUpdate(entity);
			if (flushAfterChange) {
				flush();
			}
			logger.debug("Entity of type '{}' persisted.", entityName); //$NON-NLS-1$
		} catch (DataIntegrityViolationException e) {
			if (e.getCause() instanceof ConstraintViolationException) {
				throw new DependentObjectExpection((ConstraintViolationException) e.getCause());
			} else {
				throw e;
			}
		}
	}

	@Override
	public void delete(E entity) {
		Validate.notNull(entity);

		getHibernateTemplate().delete(entity);
		if (flushAfterChange) {
			flush();
		}
		logger.debug("Entity of type '{}' deleted.", entityName); //$NON-NLS-1$
	}

	@Override
	public void detach(E entity) {
		Validate.notNull(entity);

		logger.debug("Detaching an object of type '{}' from the hibernate session.", entityName); //$NON-NLS-1$
		getHibernateTemplate().evict(entity);
	}

	protected String getPojoClassName() {
		return entityName;
	}

	protected void setFlushAfterChange(boolean flush) {
		this.flushAfterChange = flush;
	}

	@Override
	public DetachedCriteria createDetachedCriteria() {
		logger.debug("Creating a detached criteria for an entity of type: {}", entityName); //$NON-NLS-1$
		DetachedCriteria detachedCriteria = DetachedCriteria.forEntityName(entityName);

		assert (detachedCriteria != null);
		return detachedCriteria;
	}

	@Override
	public List<E> findByCriteria(DetachedCriteria detachedCriteria) {
		return findByCriteria(detachedCriteria, null);
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<E> findByCriteria(DetachedCriteria detachedCriteria, PagingConfig paging) {
		Validate.notNull(detachedCriteria);

		logger.debug("Executing find by detached criteria query..."); //$NON-NLS-1$
		if (paging != null) {
			// appending order causes
			for (OrderConfig order : paging.getOrderList()) {
				detachedCriteria.addOrder(order.isDescending() ? Order.desc(order.getProperty()) : Order.asc(order
						.getProperty()));
			}
		}

		// executing paging query
		List<E> result = null;
		if (paging != null && paging.getBeginRow() != -1) {
			logger.debug("Executing find by detached criteria paging query..."); //$NON-NLS-1$
			result = getHibernateTemplate().findByCriteria(detachedCriteria, paging.getBeginRow(), paging.getMaxResults());
		} else {
			result = getHibernateTemplate().findByCriteria(detachedCriteria);
		}

		return result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<E> findByHQL(String hql) {
		Validate.notEmpty(hql);

		logger.debug("Executing find by HQL query..."); //$NON-NLS-1$

		List<E> result = getHibernateTemplate().find(hql);
		return result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<E> findByHQL(String hql, Object[] params) {
		Validate.notEmpty(hql);
		Validate.notEmpty(params);

		logger.debug("Executing find by HQL query..."); //$NON-NLS-1$

		List<E> result = getHibernateTemplate().find(hql, params);
		return result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public E findByUniqueProperty(String propertyName, Object searchValue) {
		Validate.notEmpty(propertyName);
		Validate.notNull(searchValue);

		logger.debug("Executing find by unique property query [{}:{}]...", propertyName, searchValue); //$NON-NLS-1$

		final DetachedCriteria criteria = createDetachedCriteria();
		criteria.add(Restrictions.eq(propertyName, searchValue));

		E result = (E) getHibernateTemplate().execute(new HibernateCallback() {

			@Override
			public Object doInHibernate(Session session) throws HibernateException, SQLException {

				E entity = (E) criteria.getExecutableCriteria(session).uniqueResult();
				return entity;
			}
		});

		return result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public long getCount() {
		final DetachedCriteria criteria = createDetachedCriteria();
		criteria.setProjection(Projections.rowCount());

		Long result = (Long) getHibernateTemplate().execute(new HibernateCallback() {

			@Override
			public Object doInHibernate(Session session) throws HibernateException, SQLException {

				Long rowCount = (Long) criteria.getExecutableCriteria(session).uniqueResult();
				return rowCount;
			}
		});

		long entitiesCount = result.longValue();
		logger.debug("'{}' entities of type '{}' found.", result, entityName); //$NON-NLS-1$

		return entitiesCount;
	}

	@Override
	public E lock(E entity) {
		logger.debug("lock(E) - start"); //$NON-NLS-1$

		Validate.notNull(entity);
		getSession().buildLockRequest(LockOptions.NONE).lock(entity);

		logger.debug("lock(E) - end"); //$NON-NLS-1$
		return entity;
	}

	@SuppressWarnings("unchecked")
	@Override
	public E merge(E entity) {
		logger.debug("merge(E) - start"); //$NON-NLS-1$

		Validate.notNull(entity);
		E mergedEntity = (E) getSession().merge(entity);

		logger.debug("merge(E) - end"); //$NON-NLS-1$
		return mergedEntity;
	}

	@Override
	public E update(E entity) {
		logger.debug("update(E) - start"); //$NON-NLS-1$

		Validate.notNull(entity);
		getSession().update(entity);

		logger.debug("update(E) - end"); //$NON-NLS-1$
		return entity;
	}

	@Override
	public E attach(E entity) throws AttachEntityException {
		Validate.notNull(entity);

		String attachEntityName = entity.getClass().getName();
		try {
			logger.debug("Trying to attach entity of type '{}'...", attachEntityName); //$NON-NLS-1$

			lock(entity);
			logger.debug("Entity of type '{}' attached to the hibernate session (lock).", attachEntityName); //$NON-NLS-1$
		} catch (HibernateException e) {
			logger.warn("Error attaching entity.", e); //$NON-NLS-1$

			try {
				entity = merge(entity);

				logger
						.debug(
								"Entity of type '{}' attached to the current hibernate session (merge).", attachEntityName); //$NON-NLS-1$
			} catch (HibernateException ex) {
				String exeptionMessage = String.format(
						"Coult not attach entity of type '%s' to the current session (merge).", attachEntityName); //$NON-NLS-1$

				logger.error(exeptionMessage);
				throw new AttachEntityException(exeptionMessage, ex);
			}
		}

		return entity;
	}

	@Override
	public void clearSession() {
		getHibernateTemplate().clear();

		logger.debug("Hibernate session cleared."); //$NON-NLS-1$
	}

	@Override
	public void flush() {
		getHibernateTemplate().flush();

		logger.debug("Hibernate session flushed."); //$NON-NLS-1$
	}

	@Override
	public List<E> findByPropertyName(String propertyName, Object propertyValue) {
		Validate.notEmpty(propertyName);

		logger.debug("Executing find by property query [{}:{}]...", propertyName, propertyValue); //$NON-NLS-1$

		DetachedCriteria crit = createDetachedCriteria();
		crit.add(Restrictions.eq(propertyName, propertyValue));
		return findByCriteria(crit);
	}

	@Override
	public List<E> findAll(boolean idOrderDesc) {
		logger.debug("Finding all entities of type '{}' ordered: {}.", entityName, idOrderDesc ? "desc" : "asc"); //$NON-NLS-1$

		DetachedCriteria detachedCriteria = createDetachedCriteria();

		if (idOrderDesc) {
			detachedCriteria.addOrder(Order.desc(ID_PROPERTY_NAME));
		} else {
			detachedCriteria.addOrder(Order.asc(ID_PROPERTY_NAME));
		}

		return findByCriteria(detachedCriteria);
	}
}
